package jmine.tec.persist.schema.impl.parsers;

import java.sql.Types;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import jmine.tec.persist.schema.api.SchemaElementParser;
import jmine.tec.persist.schema.impl.elements.ColumnTypeElement;
import jmine.tec.persist.schema.impl.elements.Schema;

import org.hibernate.dialect.Dialect;
import org.hibernate.dialect.Oracle10gDialect;

/**
 * Parser de tipo de coluna.
 * 
 * @author lundberg
 */
public class ColumnTypeParser implements SchemaElementParser<ColumnTypeElement> {

    private static final Dialect DIALECT = new Oracle10gDialect();

    private Schema schema;

    private int code;

    private Pattern pattern;

    private Integer lengthIndex = null;

    private Integer precisionIndex = null;

    private Integer scaleIndex = null;

    /**
     * Construtor
     * 
     * @param schema schema
     * @param code tipo sql que deve ser lido pelo parser
     * @see java.sql.Types
     */
    public ColumnTypeParser(Schema schema, int code) {
        this.schema = schema;
        this.code = code;
        this.configure();
    }

    /**
     * {@inheritDoc}
     */
    public boolean matches(String statement) {
        return this.pattern.matcher(statement).matches();
    }

    /**
     * {@inheritDoc}
     */
    public ColumnTypeElement parse(String statement) {
        Matcher matcher = this.pattern.matcher(statement);
        if (!matcher.matches()) {
            throw new IllegalStateException();
        }
        ColumnTypeElement type = new ColumnTypeElement(this.schema);
        type.setCode(this.code);
        if (this.lengthIndex != null && matcher.groupCount() > 0) {
            type.setLength(Integer.parseInt(matcher.group(this.lengthIndex)));
        } else {
            if (Types.VARCHAR == this.code) {
                // campo do tipo "long", length máximo não determinado
                type.setLength(Integer.MAX_VALUE);
            } else if (Types.VARBINARY == this.code) {
                // campo do tipo "long raw", length máximo não determinado
                type.setLength(Integer.MAX_VALUE);
            } else {
                type.setLength(0);
            }
        }
        if (this.precisionIndex != null) {
            type.setPrecision(Integer.parseInt(matcher.group(this.precisionIndex)));
        } else {
            type.setPrecision(0);
        }
        // scaleIndex não nulo, mas não presente no matcher, é possível no caso de omissão do scale em number
        if (this.scaleIndex != null && matcher.group(this.scaleIndex) != null) {
            type.setScale(Integer.parseInt(matcher.group(this.scaleIndex)));
        } else {
            type.setScale(0);
        }
        return type;
    }

    /**
     * Configura o parser, de acordo com o tipo sql que deve ser lido.
     */
    private void configure() {
        if (Types.VARCHAR == this.code) {
            // O tipo varchar pode ser lido como varchar2 ou long dependendo do length necessário.
            this.pattern = Pattern.compile("varchar2\\((\\d+) char\\)|long", Pattern.CASE_INSENSITIVE);
            this.lengthIndex = 1;
        } else if (Types.VARBINARY == this.code) {
            // O tipo raw pode ser lido como raw ou long raw, dependendo do length necessário
            this.pattern = Pattern.compile("raw\\((\\d+)\\)|long raw", Pattern.CASE_INSENSITIVE);
            this.lengthIndex = 1;
        } else if (Types.NUMERIC == this.code) {
            // O parser deve ser capaz de ler campos numéricos escritos como scale omitido, ex.: number(20)
            this.pattern = Pattern.compile("number\\((\\d+)(?:,(\\d+))?\\)", Pattern.CASE_INSENSITIVE);
            this.precisionIndex = 1;
            this.scaleIndex = 2;
        } else {
            // Tratamento padrão, baseado nos tipos definidos no dialect do Hibernate para Oracle
            String typeName = DIALECT.getTypeName(this.code);
            this.pattern = Pattern.compile(Pattern.quote(typeName).replaceAll("\\$[lps]", "(\\d+)"), Pattern.CASE_INSENSITIVE);
            SortedMap<Integer, String> m = new TreeMap<Integer, String>();
            m.put(typeName.indexOf("$p"), "p");
            m.put(typeName.indexOf("$l"), "l");
            m.put(typeName.indexOf("$s"), "s");
            int i = 0;
            for (Map.Entry<Integer, String> entry : m.entrySet()) {
                Integer groupIndex = entry.getKey() > 0 ? ++i : null;
                if (entry.getValue().equals("l")) {
                    this.lengthIndex = groupIndex;
                } else if (entry.getValue().equals("p")) {
                    this.precisionIndex = groupIndex;
                } else {
                    this.scaleIndex = groupIndex;
                }
            }
        }
    }
}
